/*
 * cube2h265: convert cubes to h265 videos (lossy or lossless).
 * GOU Lingfeng <goulf@ion.ac.cn>
 *
 * Only 16-bit (converted to 12-bit) data are supported.
 *
 */

#include <getopt.h>
#include <iostream>
#include <memory>
#include <sys/stat.h>
#include <sys/wait.h>
#include <string.h>
#include <cstring>
#include <fstream>
#include <libgen.h>

#include "public/utils.h"
#include "image.reader.h"
#include "config-fnt.h"

struct ArgvHelper {
	constexpr static std::size_t MAX_NUM=128;
	constexpr static std::size_t MAX_LEN=32*1024;
	char* ptrs[MAX_NUM];
	char buf[MAX_LEN];
	std::size_t np, nb;
	ArgvHelper(const char* argv0): np{1} {
		auto n=snprintf(buf, MAX_LEN, "%s", argv0);
		if(n<0)
			throw;
		if(static_cast<std::size_t>(n)>=MAX_LEN)
			throw;
		ptrs[0]=buf;
		nb=0+n+1;
	}
	template<typename... Args>
		void add(const char* fmt, Args&&... args) {
			if(np+1>=MAX_NUM)
				throw;
			auto n=snprintf(buf+nb, MAX_LEN-nb, fmt, std::forward<Args>(args)...);
			if(n<0)
				throw;
			if(n+nb>=MAX_LEN)
				throw;
			ptrs[np++]=buf+nb;
			nb=nb+n+1;
		}
	char* const* get() noexcept {
		ptrs[np]=nullptr;
		return ptrs;
	}
};
struct Vec3{
	uint64_t x,y,z;

};
struct ConvParams {
	uint64_t xleft, yleft, zleft;
	uint64_t xdelta, ydelta, zdelta;
	uint64_t xright, yright, zright;
	uint64_t xsize,ysize,zsize;
	std::string hm_path;

	ConvParams():
		xleft{0}, yleft{0}, zleft{0},
		xdelta{0}, ydelta{0}, zdelta{0},
		xright{0}, yright{0}, zright{0},
		xsize{0}, ysize{0}, zsize{0}
	{ }
	bool multipleCubes() {
		return xleft+xdelta<xright || yleft+ydelta<yright || zleft+zdelta<zright;
	}
};

/* Options */
static const char* opts=":x:y:z:s:j:m:i:o::hv";
static const struct option opts_long[]=
{
	{"xrange", 1, nullptr, 'x'},
	{"yrange", 1, nullptr, 'y'},
	{"zrange", 1, nullptr, 'z'},
	{"size", 1, nullptr, 's'},
	{"jobs", 2, nullptr, 'j'},
	{"mask", 1, nullptr, 'm'},
	{"in", 1, nullptr, 'i'},
	{"out", 1, nullptr, 'o'},
	{"help", 0, nullptr, 'h'},
	{"version", 0, nullptr, 'v'},
	{nullptr, 0, nullptr, 0}
};

/////
/* Print version information and exit */
void version(const char* name) {
	std::cout<<name<<" (" PACKAGE_NAME_LONG ") " PACKAGE_VERSION "\n";
	std::cout<<"Copyright (C) 2015-2017 " PACKAGE_ORG "\n";
	std::cout<<"License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>\n\n";
	std::cout<<"Written by: " PACKAGE_AUTHORS "\n";
	exit(0);
}

/* Print usage information and exit */
void usage(const char* name, int ecode) {
	if(ecode) {
		std::cerr<<"Try '"<<name<<" --help' for more information.\n";
		exit(ecode);
	}
	std::cout<<"Usage: "<<name<<" [OPTION]... PATH_HM_ENC INPUT OUTPUT\n";
	std::cout<<
		"Convert cubes to videos for better compression.\n\n"
		"Options:\n"
		"  -h, --help       [OPTIONAL] Print this help, and do nothing else.\n"
		"  -v, --version    [OPTIONAL] Print version information and exit.\n"
		"  -m, --mask       [OPTIONAL] mask filename.\n\n"
		"Report bugs to <" PACKAGE_BUGREPORT ">\n"
		PACKAGE_NAME_LONG " home page: <" PACKAGE_URL ">\n";
	exit(ecode);
}
bool copyfile(const char *filename, const char * fileout)
{
    //用fread和fwrite函数
    //int fread( void *buffer, size_t size, size_t num, FILE *stream );
    //int fwrite(const void *buffer, size_t size, size_t count, FILE *stream);
    char buff[1024];
    FILE*fp_filename = fopen(filename, "r");
    FILE*fp_fileout = fopen(fileout, "w+");//一定要w+,这样在没有文件的情况下也能创建
                                        //r+，只能打开写，不能创建
    if (!fp_filename)
    {
        printf("copy failed: no source\n");
        return false;
    }
    if (!fp_fileout)
    {
        printf("copy failed: no target\n");
        return false;
    }
    while (!feof(fp_filename))
    {
        int len = fread(buff, sizeof(char), sizeof(buff), fp_filename);
        fwrite(buff, sizeof(char), len, fp_fileout);
    }
    fclose(fp_filename);
    fclose(fp_fileout);
    return true;
}

void createDirectory(const std::string& fulldir) {
	std::string tmpdir;
	for(size_t i=0; i<fulldir.size(); i++) {
		tmpdir.push_back(fulldir[i]);
		bool e=(i+1)==fulldir.size();
		bool s=!e && fulldir[i+1]=='/';
		if(e||s) {
			struct stat buf;
			int r=stat(tmpdir.c_str(), &buf);
			if(r==-1 || !S_ISDIR(buf.st_mode)) {
#ifdef BUILD_MINGW
				r=mkdir(tmpdir.c_str());
#else
				r=mkdir(tmpdir.c_str(), 0777);
#endif
				if(r==-1) {
					throwError("Cannot create dir '", tmpdir, "': ", strerror(errno), ".\n");
				}
			}

		}
	}
}

CubeType decode(std::vector<char>& buf, const char* input_file, int64_t* pw, int64_t* ph, int64_t* pd) {
	std::unique_ptr<ImageReader> imageReader{ImageReader::create(input_file)};
	if(!imageReader)
		throwError("Cannot read file: ", input_file);

	CubeType type;
	if(!imageReader->getType(&type))
		throwError("Cannot get image type: ", input_file);

	// switch(type) {
	// 	case CubeType::U16:
	// 		break;
	// 	case CubeType::U8:
	// 	case CubeType::U32:
	// 	case CubeType::I8:
	// 	case CubeType::I16:
	// 	case CubeType::I32:
	// 	case CubeType::F32:
	// 		throwError("Only 16 bit unsigned images supported.");
	// 	default:
	// 		throwError("Voxel type not supported");
	// }

	int64_t w, h, d;
	if(!imageReader->getSizes(&w, &h, &d))
		throwError("Cannot get image sizes: ", input_file);

	auto bpv=bytes_per_voxel(type);
	try {
		if(buf.size()<uint64_t(bpv*w*h*d))
			buf.resize(bytes_per_voxel(type)*w*h*d);
	} catch(...) {
		throwError("Failed to alloc cube memory");
	}

	if(!imageReader->readData(buf.data(), 0, 0, 0, bpv*w, bpv*w*h))
		throwError("Failed to read image data: ", input_file);

	*pw=w;
	*ph=h;
	*pd=d;
	return type;
}


///////////////////////////////////////////////////////
bool checkmask(std::vector<char> &mask_buf,Vec3 masksize,Vec3 size,Vec3 currentpos,Vec3 cubesize){
	int dx = int(size.x/float(masksize.x)+0.5);
	int dy = int(size.y/float(masksize.y)+0.5);
	int dz = int(size.z/float(masksize.z)+0.5);
	Vec3 downcube ={cubesize.x/dx,cubesize.y/dy,cubesize.z/dz};
	Vec3 min={currentpos.x/dx,currentpos.y/dy,currentpos.z/dz};
	Vec3 max={min.x+downcube.x,min.y+downcube.y,min.z+downcube.z};
	max.x=max.x>masksize.x?masksize.x:max.x;
	max.y=max.y>masksize.y?masksize.y:max.y;
	max.z=max.z>masksize.z?masksize.z:max.z;
	if(size.x-currentpos.x<cubesize.x||size.y-currentpos.y<cubesize.y||size.z-currentpos.z<cubesize.z)
		return false;
	uint64_t sum=0;
	int val =0;
	
	for(int i=min.z;i<max.z;i++){
		for( int j=min.y;j<max.y;j++){
			for(int k=min.x;k<max.x;k++){
				int index= i*masksize.x*masksize.y+j*masksize.x+k;
				val = int((unsigned char)(mask_buf[index]));
				if(val>0){
					sum++;
				}
				if(val)break;
			}
			if(val)break;
		}
		if(val)break;
	}
	if(sum)
		return false;
	return true;
}

bool existsfile(const std::string& filename){
	struct stat buffer;
	return (stat (filename.c_str(), &buffer)==0);
}

void convert(const ConvParams& pars,Vec3 cubesize, const char* output_fn, const char* stat_fn, std::vector<char>& buf, std::vector<char>& zerobuf, int ncpu,int bitdepth=16) {

	int64_t w=cubesize.x;
	int64_t h=cubesize.y;
	int64_t d=cubesize.z;

	char nameIn[1024];
	snprintf(nameIn, 1024, "/dev/shm/fnt-tmp.in.XXXXXX");
	int fdIn;
	fdIn=mkstemp(nameIn);
	if(-1==fdIn)
		throw std::system_error{errno, std::system_category(), "failed to mkstemp"};
	std::cerr<<nameIn<<"\n";
	auto cube = static_cast<const char*>(buf.data());
	auto zero=static_cast<const char*>(zerobuf.data());

	int multi = 16/bitdepth;
	auto ystride=w*sizeof(uint16_t)/multi;
	int64_t resizew = int(w / 8.0f + 0.5) * 8;
	int64_t resizeh = int(h / 8.0f + 0.5) * 8;
	std::cout<< "width: " << resizew << "height: "<< resizeh <<std::endl;
	for(int64_t z=0; z<d; z++) {
		for(int64_t y=0; y<h; y++) {
			auto r=write(fdIn, cube+ystride*h*z+ystride*y, w*sizeof(uint16_t)/multi);
			if(pars.xdelta-w)
				auto r = write(fdIn, zero, (resizew-w) * sizeof(uint16_t)/multi);

			if(r<0)
				throw std::system_error{errno, std::system_category(), "failed to write"};
			if(static_cast<std::size_t>(r)!=w*sizeof(uint16_t)/multi)
				throw std::system_error{errno, std::system_category(), "failed to write"};
		}

		for (int y = h; y < resizeh; y++) {
			auto r = write(fdIn, zero, resizew * sizeof(uint16_t)/multi);
		}
	}
	if(close(fdIn)==-1)
		throw std::system_error{errno, std::system_category(), "failed to close"};

	auto pid=fork();
	if(pid==-1) {
		throw std::system_error{errno, std::system_category(), "failed to fork"};
	} 
	else if(pid==0) {
		ArgvHelper argv{pars.hm_path.c_str()};
		argv.add("--InputFile=%s", nameIn);
		argv.add("--BitstreamFile=%s", output_fn);
		argv.add("--SourceWidth=%d", resizew);
		argv.add("--SourceHeight=%d", resizeh);
		argv.add("--FramesToBeEncoded=%d", d);
		argv.add("--InputBitDepth=%d",bitdepth);
		argv.add("--InputChromaFormat=400");
		argv.add("--FrameRate=24");
		argv.add("--QP=30");
		argv.add("--Profile=monochrome16");
		argv.add("--ExtendedPrecision");
		argv.add("--QuadtreeTULog2MaxSize=5");
		argv.add("--ConformanceWindowMode=0");
		argv.add("--SEIDecodedPictureHash=0");
		argv.add("--Level=5.1");
		argv.add("--QuadtreeTUMaxDepthIntra=3");
		argv.add("--QuadtreeTUMaxDepthInter=3");
		argv.add("--IntraPeriod=1");
		argv.add("--GOPSize=1");
		argv.add("--Frame1=I 1 0 0 0 0 0 0.57 0 0 0 1 0 0");
		argv.add("--DecodingRefreshType=0");

		auto er=execv(pars.hm_path.c_str(), argv.get());
		(void)er;
		std::cout<<pars.hm_path.c_str()<<std::endl;
		throw std::system_error{errno, std::system_category(), "failed to exec"};
	} 
	else {
		int wstatus;
		if(waitpid(pid, &wstatus, 0)==-1)
			throw std::system_error{errno, std::system_category(), "failed to waitpid"};
		if(-1==unlink(nameIn))
			throw std::system_error{errno, std::system_category(), "failed to unlink"};
		if(!WIFEXITED(wstatus))
			throw std::runtime_error{"failed to waitpid, not exited"};
		if(WEXITSTATUS(wstatus)!=0)
			exit(WEXITSTATUS(wstatus));
	}

}


void convert(const ConvParams& pars, const char* input_fn, const char* output_fn, const char* stat_fn, std::vector<char>& buf, std::vector<char>& zerobuf, int ncpu) {
	int64_t w, h, d;
	auto type=decode(buf, input_fn, &w, &h, &d);
	std::cout<<input_fn<<std::endl;
	switch(type) {
		case CubeType::U16:
			convert(pars,{w,h,d},output_fn,stat_fn,buf,zerobuf,ncpu,16);		
			break;
		case CubeType::U8:
			convert(pars,{w,h,d},output_fn,stat_fn,buf,zerobuf,ncpu,8);
			break;
		case CubeType::U32:
		case CubeType::I8:
		case CubeType::I16:
		case CubeType::I32:
		case CubeType::F32:
			throwError("Only 16 bit unsigned images supported.");
		default:
			throwError("Voxel type not supported");
	}
	

	
}
void copyblack(const ConvParams& pars,const char* output_fn,Vec3 cubesize,int ncpu){
	std::string blackname = "blackdir/black"+
	std::to_string(int(cubesize.x))+"x"+
	std::to_string(int(cubesize.y))+"x"+
	std::to_string(int(cubesize.z))+
	".hevc";
	if(existsfile(blackname.c_str())){
		copyfile(blackname.c_str(),output_fn);
	}else{
		std::cout<<"CREATE:"<<blackname<<std::endl;

		std::vector<char> buf;
		int64_t w, h, d;
		auto type=decode(buf, "../y00000000.x00000000.nrrd", &w, &h, &d);
		std::vector<char> zerobuf;
		switch(type){
			case CubeType::U16:
				convert(pars,cubesize,blackname.c_str(),nullptr,buf,zerobuf,ncpu,16);
			break;
			case CubeType::U8:
				convert(pars,cubesize,blackname.c_str(),nullptr,buf,zerobuf,ncpu,8);
			break;
			default:
			break;
		}
		copyfile(blackname.c_str(),output_fn);

	}
}
void convertCube(const ConvParams& pars ,std::string infilename, int64_t w, int64_t h, int64_t d,std::string outfilename,int ncpu=1) {
	std::vector<char> buf;
	auto type = decode(buf, infilename.c_str(), &w, &h, &d);
	std::vector<char> zerobuf;
	convert(pars, { pars.xdelta,pars.ydelta,pars.zdelta }, outfilename.c_str(), nullptr, buf, zerobuf, ncpu);
}
void createblack(const ConvParams& pars,int ncpu){
	Vec3 size={pars.xsize,pars.ysize,pars.zsize};
	Vec3 cubesize={pars.xdelta,pars.ydelta,pars.zdelta};
	createDirectory("blackdir");


	std::string blackname = "blackdir/black"+
	std::to_string(int(cubesize.x))+"x"+
	std::to_string(int(cubesize.y))+"x"+
	std::to_string(int(cubesize.z))+
	".hevc";
	if(!existsfile(blackname.c_str())){
		std::vector<char> buf;
		int64_t w, h, d;
		auto type=decode(buf, "../y00000000.x00000000.nrrd", &w, &h, &d);
		std::vector<char> zerobuf;
		convert(pars,{pars.xdelta,pars.ydelta,pars.zdelta},blackname.c_str(),nullptr,buf,zerobuf,ncpu);
	}
}
int main(int argc, char* argv[]) {

	std::string infilename = "";
	std::string outfilename = "";

	ConvParams pars{};
	int ncpu = 1;
	std::string maskfn;
	int opt;
	int prev_optind = optind;
	while ((opt = getopt_long(argc, argv, opts, opts_long, nullptr)) != -1) {
		std::cout<<(char)opt<<std::endl;
		try {
			switch (opt) {
			case '?':
				throwError("unrecognized option '", argv[prev_optind], "'\n");
			case ':':
				throwError("option '", argv[prev_optind], "' requires an argument\n");
			case 'h':
				usage(argv[0], 0);
			case 'v':
				version(argv[0]);
			case 'x':
				if (!parseTuple(optarg, &pars.xleft, &pars.xdelta, &pars.xright))
					throwError("invalid argument for '", argv[prev_optind], "'\nPlease give 3 integers separated by ':'.\n");
				if (pars.xleft >= pars.xright)
					throwError("invalid argument for '", argv[prev_optind], "'\nPlease give a valid range.\n");
				if (pars.xdelta == 0)
					throwError("invalid argument for '", argv[prev_optind], "'\nSetep size must be positive.\n");
				break;
			case 'y':
				if (!parseTuple(optarg, &pars.yleft, &pars.ydelta, &pars.yright))
					throwError("invalid argument for '", argv[prev_optind], "'\nPlease give 3 integers separated by ':'.\n");
				if (pars.yleft >= pars.yright)
					throwError("invalid argument for '", argv[prev_optind], "'\nPlease give a valid range.\n");
				if (pars.ydelta == 0)
					throwError("invalid argument for '", argv[prev_optind], "'\nSetep size must be positive.\n");
				break;
			case 'z':
				if (!parseTuple(optarg, &pars.zleft, &pars.zdelta, &pars.zright))
					throwError("invalid argument for '", argv[prev_optind], "'\nPlease give 3 integers separated by ':'.\n");
				if (pars.zleft >= pars.zright)
					throwError("invalid argument for '", argv[prev_optind], "'\nPlease give a valid range.\n");
				if (pars.zdelta == 0)
					throwError("invalid argument for '", argv[prev_optind], "'\nSetep size must be positive.\n");
				break;
			case 's':
				if (!parseTuple(optarg, &pars.xsize, &pars.ysize, &pars.zsize))
					throwError("invalid argument for '", argv[prev_optind], "'\nPlease give 3 integers separated by ':'.\n");
				break;
			case 'j':
				if (optarg) {
					if (!parseTuple<int>(optarg, &ncpu))
						throwError("invalid argument for '", argv[prev_optind], "'.\nPlease give an integer.\n");
					if (ncpu <= 0)
						throwError("invalid argument for '", argv[prev_optind], "'.\nNeed a positive integer.\n");
				}
				else {
					ncpu = getNumberOfCpus();
					if (ncpu<1)
						ncpu = 1;
				}
				break;
			case 'm':
				pars.hm_path = argv[prev_optind + 2];
				break;
			case 'i':
				infilename = argv[prev_optind + 1];
				break;
			case 'o':
				outfilename = argv[prev_optind + 1];
				break;
			default:
				throwError("Unexpected error.\n");
			}
		}
		catch (std::exception& e) {
			std::cerr << argv[0] << ": " << e.what();
			usage(argv[0], -1);
		}

		prev_optind = optind;
	}
	std::cout << pars.hm_path << std::endl;
	std::cout << infilename << std::endl;
	std::cout << outfilename << std::endl;
	if (existsfile(infilename.c_str())) {
		convertCube(pars, infilename, pars.xdelta, pars.ydelta, pars.zdelta, outfilename, ncpu);
	}


	return 0;
}

